Skip to content

bun-types: declare the absent values of spawnSync().exitCode and of the socket address and TLS getters - #48411

Open
robobun wants to merge 1 commit into
mainfrom
robobun/e8bf2afd/bun-types-not-set-values
Open

robobun wants to merge 1 commit into
mainfrom
robobun/e8bf2afd/bun-types-not-set-values

Conversation

@robobun

@robobun robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Fix

  • Declare exitCode: number | null, | undefined for the seven address properties, getServername() and getPeerX509Certificate(), and getTLSVersion(): string | null. The JSDoc says when each value is absent.
  • Correct because no runtime value changes, and node:child_process and node:net already read these values.
  • Verified: test/integration/bun-types/bun-types.test.ts. The new fixture lines give 12 diagnostics on main.
  • Self-reviewed: 7 concerns raised, 6 addressed. Not done: a tracking issue for the members left out. The Notes list them.

Background

Downsides

  • Users of @types/bun get a strictNullChecks error where they read these values without a check: 8 places in test/ here. Three took a signal death for a number.
  • Other members of Bun.Socket and Bun.udpSocket keep the same gap.
  • No cost at run time. The compressed declarations in the binary grow by 4 bytes.
Notes

Runtime values, on 1.4.4-canary.1+2596ac5d3 (Linux x64) and on 1.4.3

Read Value
Bun.spawnSync(...).exitCode after SIGKILL, and after timeout or maxBuffer when the signal ends the child null
All six address properties of a socket of Bun.listen({ unix }) or Bun.connect({ unix }) undefined
The same six on a TCP socket, first read in close, after close, or in connectError undefined
remoteFamily, remoteAddress, remotePort in open, when the peer wrote and reset before the accept undefined
remotePort on a TCP socket after close, also when it was read in open undefined
ws.remoteAddress over Bun.serve({ unix }), and on a TCP server when the first read is in close undefined
getTLSVersion() on a socket without TLS, and after a TLS socket closes null
getServername() on a socket without TLS, after close, and when the client sent no server name undefined
getPeerX509Certificate() on a TLS server socket whose client sent no certificate, and after close undefined

A child that handles the signal of timeout can still exit with a code: sh -c "trap 'exit 0' TERM; sleep 5 & wait" with timeout: 200 gives exitCode: 0 and exitedDueToTimeout: true. So the JSDoc of exitCode names the signal, not the option, as the cause.

Why the declaration is the side that changes

  • exitCode: src/runtime/api/bun/subprocess.rs:1310 returns null unless the child exited. spawnsync-no-microtask-drain.test.ts:87 and spawn-kill-signal.test.ts pin null. src/js/node/child_process.ts:588 maps it to Node's status: null. The table of built-ins already says exitCode: b.f64.nullable (src/codegen/builtins/host/globals.builtins.ts:1129).
  • Socket addresses: the schema already says string | undefined and u16.optional (src/runtime/socket/socket_body.bind.ts:10, :69-78). src/js/node/net.ts:3137-3165 depends on undefined for a Unix domain socket, as Node does. socket.test.ts:5135 pins undefined.
  • ServerWebSocket.remoteAddress: src/runtime/server/server.bind.ts:159 says string | undefined. websocket-server.test.ts:3768 pins that a value read in open is still there in close. The schema check in bun-types.test.ts listed this row as a known difference. The row is gone.
  • @types/node declares the Node equivalents the same way: SpawnSyncReturns.status: number | null, remoteAddress: string | undefined, localAddress?: string.

Members with the same gap that this PR does not change

Checked on the same canary build.

Member Declared Runtime
Socket#getPeerCertificate() PeerCertificate null without TLS. undefined on a TLS server whose client sent no certificate, and after close
Socket#getCertificate() PeerCertificate | object | null undefined without TLS, on a TLS client without a certificate, and after close
Socket#getCipher() CipherNameAndProtocol undefined without TLS and after close
Socket#getSharedSigalgs() string[] undefined without TLS, null after close
Socket#exportKeyingMaterial() declared twice, as Buffer and as void undefined without TLS and after close
Socket#upgradeTLS() [raw, tls] undefined on a socket that has TLS already
udp.Socket#port, #address, udp.ConnectedSocket#remoteAddress number, SocketAddress, SocketAddress undefined when the first read is after close()

The schema check

Checks run

  • bun test test/integration/bun-types/bun-types.test.ts: 30 pass. With the two .d.ts files of main: the fixture case fails with 12 TS2344 diagnostics (spawn.ts:192-193, tcp.ts:177-185, websocket.ts:357) and the schema check prints the ServerWebSocket row.
  • tsc --noEmit in test/: the declarations give 8 new diagnostics (3 in bun-connect-x509.test.ts, and one each in compile-trim-features.ts, react-compiler/expected.ts, host-export-callers.test.ts, socket-dns-error.test.ts, websocket-server.test.ts). After the test edits there are 0. The five projects that the Lint workflow type-checks: 0 new.
  • host-export-callers.test.ts treated a signal death of git grep as "no match" (null > 1 is false). It now throws.
  • With a debug build and a release build of this branch: spawn-kill-signal.test.ts, spawn-maxbuf.test.ts, socket-dns-error.test.ts, websocket-server.test.ts -t remoteAddress, compile-trim-runtime.test.ts, bun-connect-x509.test.ts (the test that needs example.com fails here without network, as on main), test/aot/builtins (the same result as main) and cargo test -p bun_aot --test ir_tests lower.
  • Size: the two .d.ts files grow by 3203 bytes of JSDoc. Without comments, as the binary embeds them, they grow by 122 bytes, or 4 bytes after zstd (the build's own withoutComments and compression settings).
  • The JSDoc does not name Windows named pipes. The code gives them no address (InternalSocket::Pipe, src/uws_sys/socket.rs:698-726), but I did not run Windows.

…he socket address and TLS getters

The declarations left out the value that the runtime returns when there is
none, so a program that passed `tsc` read `null` or `undefined` as a number
or a string.

- `SyncSubprocess.exitCode` is `number | null`: `null` when a signal ended
  the child.
- The six address properties of `Bun.Socket` and
  `ServerWebSocket.remoteAddress` are `T | undefined`: a Unix domain socket
  has no IP address, and a socket that is not connected has none to report.
- `Socket.getTLSVersion()` is `string | null`. `getServername()` and
  `getPeerX509Certificate()` can return `undefined`.
- The JSDoc of each member says when the value is absent.
- The schema check no longer lists `ServerWebSocket.remoteAddress` as a
  known difference.
- The docs copy of `SyncSubprocess` matches the declaration.
- Tests that read these values as always present handle the absent value.
  The casts for `exitCode: null` are gone.
@robobun
robobun requested a review from alii as a code owner November 9, 2026 19:26
@robobun

robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 6:16 PM PT - Nov 9th, 2026

✅ @robobun, your commit f62ef0b52851f7178a89c8958e11cf175ce9717e passed in Build #139460! 🎉


🧪   To try this PR locally:

bunx bun-pr 48411

That installs a local version of the PR into your bun-48411 executable, so you can run:

bun-48411 --bun

@github-actions github-actions Bot added the claude label Nov 9, 2026
@robobun

robobun commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: open. CI is green on f62ef0b528 (Buildkite and the GitHub checks). The PR waits for a review.

How I reproduced it (Bun 1.4.4-canary.1+2596ac5d3 and 1.4.3, Linux x64). Each program below passes tsc --strict against the declarations of main.

// 1. A signal ends the child: exitCode is null, declared number.
const r = Bun.spawnSync({ cmd: [process.execPath, "-e", "process.kill(process.pid, 'SIGKILL')"] });
console.log(r.exitCode, r.signalCode, r.exitCode > 1); // null "SIGKILL" false

// 2. A Unix domain socket: all six address properties are undefined, declared string / number.
Bun.listen({ unix: path, socket: { open(s) { console.log(s.remoteAddress, s.remotePort, s.localAddress); }, data() {} } });
await Bun.connect({ unix: path, socket: { data() {} } }); // undefined undefined undefined

// 3. A TCP WebSocket, first read in close: undefined, declared string.
Bun.serve({
  port: 0,
  fetch: (req, server) => (server.upgrade(req) ? undefined : new Response("no")),
  websocket: { message() {}, close(ws) { ws.remoteAddress.toUpperCase(); } }, // TypeError: undefined is not an object
});

Type proof: bun test test/integration/bun-types/bun-types.test.ts gives 30 pass on this branch. With bun.d.ts and serve.d.ts of main, the new fixture lines give 12 TS2344 diagnostics.

@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

Public socket and subprocess declarations now represent values that may be absent. Documentation, type fixtures, and related tests were updated to reflect the nullable or optional types and their documented cases.

Changes

Socket Endpoint and TLS Types

Layer / File(s) Summary
Socket endpoint and TLS contracts with type checks
packages/bun-types/bun.d.ts, packages/bun-types/serve.d.ts, test/integration/bun-types/*, test/js/bun/http/bun-connect-x509.test.ts, test/js/bun/net/socket-dns-error.test.ts, test/js/bun/websocket/websocket-server.test.ts
Socket endpoint properties, TLS accessors, peer certificates, and ServerWebSocket.remoteAddress now allow absent values in their types. The documentation describes the cases for absent values. Type fixtures and related tests check these types and cases.

Subprocess Exit Results

Layer / File(s) Summary
Nullable exit-code contract and dependent checks
docs/runtime/child-process.mdx, packages/bun-types/bun.d.ts, test/bundler/compile-trim-features.ts, test/cli/lint/oracle/react-compiler/expected.ts, test/integration/bun-types/fixture/spawn.ts, test/internal/source-lints/host-export-callers.test.ts, test/js/bun/spawn/*
SyncSubprocess.exitCode now permits null, and its documentation describes signal termination. Related fixtures and helpers account for nullable exit codes. Spawn tests assert nullable results without casts; the gitGrep check now treats null as an error result.

Possibly related issues

Possibly related PRs

  • oven-sh/bun#46820: Both PRs address the spawnSync termination result contract, including max-buffer termination.
  • oven-sh/bun#46789: Both PRs concern type comparison for nullable and optional values, including ServerWebSocket.remoteAddress.
Merge Risk: Minimal · up to f62ef

The nullable declarations and dependent checks are mergeable after normal checks; neither investigated concern establishes a new failure from this change.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly summarizes the main change: updating Bun type declarations for values that may be absent at runtime.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description explains the problem, the changes, and how the author verified them. It does not use the template’s exact headings, but it provides the required information and substantial verificatio…
Full details: Description check

Explanation

The description explains the problem, the changes, and how the author verified them. It does not use the template’s exact headings, but it provides the required information and substantial verification details.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 2596ac5 and f62ef0b.

Files selected for processing: 15

  • docs/runtime/child-process.mdx
  • packages/bun-types/bun.d.ts
  • packages/bun-types/serve.d.ts
  • test/bundler/compile-trim-features.ts
  • test/cli/lint/oracle/react-compiler/expected.ts
  • test/integration/bun-types/bun-types.test.ts
  • test/integration/bun-types/fixture/spawn.ts
  • test/integration/bun-types/fixture/tcp.ts
  • test/integration/bun-types/fixture/websocket.ts
  • test/internal/source-lints/host-export-callers.test.ts
  • test/js/bun/http/bun-connect-x509.test.ts
  • test/js/bun/net/socket-dns-error.test.ts
  • test/js/bun/spawn/spawn-kill-signal.test.ts
  • test/js/bun/spawn/spawn-maxbuf.test.ts
  • test/js/bun/websocket/websocket-server.test.ts
Files with no reviewable changes: 1

  • test/integration/bun-types/bun-types.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant